use std::{collections::{HashMap, HashSet}, fs::{File, create_dir_all, remove_dir_all, remove_file, rename}, io::Write, path::{Path, PathBuf}};

use wtask_app::{AppTaskAdd, AppTaskCheck, AppTaskDownload, TaskAddMeta, WTASK_INSTALL_FILES, WTaskSubmitMetaBase, cli_app, msg_func_none};
use wtask_base::{app::get_wtask_data_dir, cmd::{create_file, file_to_x_power, file_unzip}, error::{WError, WResult}, export::{Value, json, log_debug}};

use crate::{basic::MachineConfigs, handle::flow::{basic::{FlowData, FlowNodeData, FlowNodeDataMeta, FlowShellMeta}, run::{FlowGraph, FlowGraphEdges}}};



struct DataNodeSingle {
    node_info: String,
    res_last_log: bool,
    cmd: String,
    args: HashMap<String, String>,
}

// node_id -> (node_info, res_last_log, cmd, args)
type DataNodes = HashMap<String, DataNodeSingle>;
// machine_id, task_id, task_version
type MachineTasks = HashSet<(String, String, String)>;
// node_id_shell -> node_meta
type DataShells = HashMap<String, FlowShellMeta>;
// node_id_args -> arg_meta
type DataArgs = HashMap<String, WTaskSubmitMetaBase>;


// machine tasks: { (machine_id, task_id, task_version) }
// datas: task/shell/arg with node_info
struct BasicData {
    machine_tasks: MachineTasks,
    data_nodes: DataNodes,
    data_shells: DataShells,
    data_args: DataArgs,
}

impl TryFrom<HashMap<String, FlowNodeData>> for BasicData {
    type Error = WError;
    fn try_from(value: HashMap<String, FlowNodeData>) -> Result<Self, Self::Error> {
        let mut machine_tasks= HashSet::new();
        let mut data_nodes = HashMap::new();
        let mut data_shells = HashMap::new();
        let mut data_args = HashMap::new();
        for (node_id, node_single) in value.into_iter() {
            let node_now = FlowNodeData::parse_meta(
                &node_single.cls,
                node_single.args,
                node_single.meta
            )?;
            let node_info = format!("{}({})", node_single.name, node_id);
            match node_now {
                FlowNodeDataMeta::Task(mut task_data) => {
                    let machine_temp = task_data.machines.into_iter().next().ok_or(WError::Empty)?;
                    let args = task_data.args.remove(&machine_temp).unwrap_or_default();
                    data_nodes.insert(node_id, DataNodeSingle {
                        node_info,
                        res_last_log: false,
                        cmd: format!("{}.{}/{}", task_data.id, task_data.version, WTASK_INSTALL_FILES[3]),
                        args
                    });
                    machine_tasks.insert((machine_temp, task_data.id, task_data.version));
                },
                FlowNodeDataMeta::Shell(shell_data) => {
                    data_nodes.insert(node_id.clone(), DataNodeSingle {
                        node_info,
                        res_last_log: shell_data.res_last_log,
                        cmd: format!("shells/{}.sh", node_id),
                        args: HashMap::with_capacity(0)
                    });
                    data_shells.insert(node_id, shell_data);
                },
                FlowNodeDataMeta::Arg(arg_data) => {
                    data_args.insert(format!("node_{}", node_id), arg_data);
                    data_nodes.insert(node_id, DataNodeSingle {
                        node_info,
                        res_last_log: false,
                        cmd: String::new(),
                        args: HashMap::with_capacity(0)
                    });
                }
            }
        }
        Ok(BasicData { machine_tasks, data_nodes, data_shells, data_args })
    }
}


async fn download_wtask_files(dir_task: &Path, machine_tasks: MachineTasks, machine_configs: &MachineConfigs) -> WResult<()> {
    for (machine_id, app, version) in machine_tasks {
        // meta
        let dir_result = dir_task.join(format!("{}.{}", app, version));
        let app_req = AppTaskDownload {
            app: Some(app),
            version: Some(version),
            dir: Some(dir_task.to_path_buf())
        };
        let config = machine_configs
            .get(&machine_id)
            .ok_or(WError::Empty)?;
        // download
        let app_res: PathBuf = cli_app(app_req, config, None, msg_func_none)
            .await?
            .ok_or(WError::Path)?
            .into();
        // unzip
        file_unzip(&app_res, &dir_result)?;
        remove_file(app_res)?;
    }
    Ok(())
}


fn args_v_replace(mut inp: String, nodes_prev: &HashSet<String>) -> String {
    for node_id_prev in nodes_prev {
        inp = inp.replace(&format!("${}$", node_id_prev), &format!("${{node_{}}}", node_id_prev));
    }
    inp
}


fn create_shell_files(dir_task: &Path, file_prefix: &str, data_shells: DataShells, data_nodes: &DataNodes, edges: &FlowGraphEdges) -> WResult<()> {
    let dir_shells = dir_task.join("shells");
    create_dir_all(&dir_shells)?;
    for (node_id, shell_meta) in data_shells.into_iter() {
        let f = dir_shells.join(format!("{}.sh", node_id));
        let node_temp = data_nodes.get(&node_id).ok_or(WError::Empty)?;
        let nodes_prev = edges.edges_parent
            .get(&node_id)
            .ok_or(WError::Empty)?;
        let cmd = args_v_replace(shell_meta.cmd, nodes_prev);
        create_file(&f, &format!("{}# {}\n\n\n{}", file_prefix, node_temp.node_info, cmd))?;
        file_to_x_power(&f)?;
    }
    Ok(())
}


fn create_file_readme(dir_task: &Path, info: String) -> WResult<()> {
    let file_readme = dir_task.join(WTASK_INSTALL_FILES[0]);
    create_file(&file_readme, &info)?;
    Ok(())
}


fn create_file_meta(dir_task: &Path, name: &str, groups: Vec<String>, data_args: DataArgs) -> WResult<()> {
    let file_meta = dir_task.join(WTASK_INSTALL_FILES[1]);
    let tags = groups
        .into_iter()
        .map(| mut v | {
            if let Some(i) = v.find('@') {
                let _ = v.split_off(i);
            }
            v
        }).collect::<Vec<_>>();
    create_file(&file_meta, &json!({
        "name": name,
        "tags": tags,
        "args": data_args,
    }).to_string())?;
    Ok(())
}


fn create_file_install(dir_task: &Path, file_prefix: &str, machine_tasks: &MachineTasks) -> WResult<()> {
    let file_install = dir_task.join(WTASK_INSTALL_FILES[3]);
    let mut f = File::create(file_install)?;
    // prefix
    f.write_all(file_prefix.as_bytes())?;
    // install
    for (_, task_id, task_version) in machine_tasks.iter() {
        f.write_all(format!(
            "${{wtask_src_path}}/{}.{}/{}\n\n",
            task_id,
            task_version,
            WTASK_INSTALL_FILES[3]
        ).as_bytes())?;
    }
    Ok(())
}




/// ordered, log
/// cmd1
/// cmd2
fn create_file_run_orderd(
    nodes_id: HashSet<String>,
    data_nodes: &mut DataNodes,
    edges: &FlowGraphEdges,
    nodes_done: &mut HashMap<String, bool>,
    data_res: &mut Vec<String>
) -> WResult<()> {
    log_debug!("run nodes: {:?}", nodes_id);
    if nodes_id.is_empty() {
        return Ok(());
    }
    for node_id in nodes_id.iter() {
        let node_temp = data_nodes
            .remove(node_id)
            .ok_or(WError::Empty)?;
        if node_temp.cmd.is_empty() {
            // arg node, skip
            continue;
        }

        let node_info = node_temp.node_info;
        let node_cmd = node_temp.cmd;
        let nodes_prev = edges.edges_parent
            .get(node_id)
            .ok_or(WError::Empty)?;
        let node_args = node_temp.args
            .into_iter()
            .map(| (k, v) | format!("{}=\"{}\"", k, args_v_replace(v, nodes_prev)))
            .collect::<Vec<_>>()
            .join("\n");
        let r = if node_temp.res_last_log {
            format!(r#"
# {node_info}
# log
log_{node_id}=$(mktemp)
trap 'rm -f "$log_{node_id}"' EXIT
# args
{node_args}
# run
${{wtask_src_path}}/{node_cmd} | tee "$log_{node_id}"
# res
node_{node_id}=$(tail -n 1 "$log_{node_id}")
"#)
        } else {
            // 获取当前文件夹，作为该节点的输出
            format!(r#"
# {node_info}
# args
{node_args}
# run
${{wtask_src_path}}/{node_cmd}
# res
node_{node_id}="`pwd`"
"#)
        };
        data_res.push(r);
    }
    nodes_done.extend(nodes_id.clone().into_iter().map(| v | (v, true)));
    for node_id in nodes_id {
        let nodes_id_next = FlowGraph::find_next_node(
            &node_id,
            &edges.edges_child,
            &edges.edges_parent,
            nodes_done
        )?;
        create_file_run_orderd(nodes_id_next, data_nodes, edges, nodes_done, data_res)?;
    }
    Ok(())
}


fn create_file_run(dir_task: &Path, file_prefix: &str, mut data_nodes: DataNodes, edges: &FlowGraphEdges) -> WResult<()> {
    // get file
    let mut data_res = Vec::new();
    let mut nodes_done = HashMap::new();
    let nodes_id_start = FlowGraph::find_start_node(&edges.edges_child, &data_nodes)?;
    
    create_file_run_orderd(
        nodes_id_start,
        &mut data_nodes,
        edges,
        &mut nodes_done,
        &mut data_res
    )?;

    // write file
    let file_run = dir_task.join(WTASK_INSTALL_FILES[2]);
    create_file(&file_run, &format!(
        "{}{}",
        file_prefix,
        data_res.join("\n\n")
    ))?;

    Ok(())
}


pub(super) async fn convert_to_wtask(dir_task: &Path, flow_data: FlowData, machine_configs: MachineConfigs, machine_save: String) -> WResult<Value> {
    // dir result
    let _ = remove_dir_all(dir_task);
    create_dir_all(dir_task)?;


    // basic data
    let basic_data = BasicData::try_from(flow_data.nodes)?;
    let file_prefix = format!(
        "#!/bin/bash\nset -e\n\n# wTask flow convert\n# {}({})\n\n\n",
        flow_data.setting.name,
        flow_data.setting.id,
    );
    let edges = FlowGraph::build_edges(flow_data.edges);
    
    // install
    create_file_install(
        dir_task,
        &file_prefix,
        &basic_data.machine_tasks
    )?;

    // download task files
    download_wtask_files(
        dir_task,
        basic_data.machine_tasks,
        &machine_configs
    ).await?;

    // shell files
    create_shell_files(
        dir_task,
        &file_prefix,
        basic_data.data_shells,
        &basic_data.data_nodes,
        &edges
    )?;

    // flow file prepare success
    
    // task basic file left
    
    // readme
    create_file_readme(
        dir_task,
        flow_data.setting.info
    )?;

    // meta
    create_file_meta(
        dir_task,
        &flow_data.setting.name,
        flow_data.setting.groups,
        basic_data.data_args
    )?;

    // run
    create_file_run(
        dir_task,
        &file_prefix,
        basic_data.data_nodes,
        &edges,
    )?;

    // check
    AppTaskCheck {
        path: dir_task.to_path_buf(),
        no_install: false,
        keep_log: false,
    }.run_check(msg_func_none)?;

    // upload
    let app = AppTaskAdd {
        data: TaskAddMeta {
            path: dir_task.to_string_lossy().to_string(),
            no_wait: true,
            version: None,
        },
        archv: false,
    };
    let config = machine_configs
        .get(&machine_save)
        .ok_or(WError::Empty)?;
    let app_res = cli_app(app, config, None, msg_func_none).await?
        .and_then(| mut v | v.apps.pop())
        .ok_or(WError::Empty)?;

    // mv task zip file
    let file_name = format!("{}.{}.zip", app_res.id, app_res.version);
    let file_task_now = get_wtask_data_dir("file", true)?.join(&file_name);
    let file_task_ori = dir_task
        .parent()
        .ok_or(WError::Path)?
        .join(file_name);
    log_debug!("mv {} {:?} {:?}", file_task_ori.exists(), file_task_ori, file_task_now);
    rename(file_task_ori, file_task_now)?;

    Ok(json!({
        "id": app_res.id,
        "name": app_res.name,
        "version": app_res.version,
    }))
}